using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace Microsoft.Maui.Graphics
{
    public class PathBuilder
    {
        public static PathF Build(string definition)
        {
            if (string.IsNullOrEmpty(definition))
                return new PathF();

            var pathBuilder = new PathBuilder();
            var path = pathBuilder.BuildPath(definition);
            return path;
        }

        private readonly Stack<string> _commandStack = new Stack<string>();
        private bool _closeWhenDone;
        private char _lastCommand = '~';
        private PointF? _lastCurveControlPoint;
        private PointF? _lastMoveTo;

        private PathF _path;
        private PointF? _relativePoint;
        
        private bool NextBoolValue
        {
            get
            {
                string vValueAsString = _commandStack.Pop();

                if ("1".Equals(vValueAsString))
                {
                    return true;
                }

                return false;
            }
        }

        private float NextValue
        {
            get
            {
                string vValueAsString = _commandStack.Pop();
                try
                {
                    return ParseFloat(vValueAsString);
                }
                catch (Exception exc)
                {
                    Logger.Warn("Error parsing a path value: " + vValueAsString, exc);
                    throw new Exception("Error parsing a path value.", exc);
                }
            }
        }

        public static float ParseFloat(string value)
        {
            if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
            {
                return number;
            }

            // Note: Illustrator will sometimes export numbers that look like "5.96.88", so we need to be able to handle them
            var split = value.Split(new[] {'.'});
            if (split.Length > 2)
            {
                if (float.TryParse($"{split[0]}.{split[1]}", NumberStyles.Any, CultureInfo.InvariantCulture, out number))
                {
                    return number;
                }
            }

            string stringValue = GetNumbersOnly(value);
            if (float.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
            {
                return number;
            }

            throw new Exception($"Error parsing {value} as a float.");
        }

        private static string GetNumbersOnly(string value)
        {
            var builder = new StringBuilder(value.Length);
            foreach (char c in value)
            {
                if (char.IsDigit(c) || c == '.' || c == '-')
                {
                    builder.Append(c);
                }
            }

            return builder.ToString();
        }

        public PathF BuildPath(string pathAsString)
        {
            try
            {
                _lastCommand = '~';
                _lastCurveControlPoint = null;
                _path = null;
                _commandStack.Clear();
                _relativePoint = new PointF(0, 0);
                _closeWhenDone = false;

#if DEBUG_PATH
                Logger.Debug(aPathString);
#endif
                pathAsString = pathAsString.Replace("Infinity", "0");
                pathAsString = Regex.Replace(pathAsString, "([a-zA-Z])", " $1 ");
                pathAsString = pathAsString.Replace("-", " -");
                pathAsString = pathAsString.Replace(" E  -", "E-");
                pathAsString = pathAsString.Replace(" e  -", "e-");
#if DEBUG_PATH
                Logger.Debug(aPathString);
#endif
                string[] args = pathAsString.Split(new[] {' ', '\r', '\n', '\t', ','}, StringSplitOptions.RemoveEmptyEntries);
                for (int i = args.Length - 1; i >= 0; i--)
                {
                    string entry = args[i];
                    char c = entry[0];
                    if (char.IsLetter(c))
                    {
                        if (entry.Length > 1)
                        {
                            entry = entry.Substring(1);
                            if (char.IsLetter(entry[0]))
                            {
                                if (entry.Length > 1)
                                {
                                    _commandStack.Push(entry.Substring(1));
#if DEBUG_PATH
                                        Logger.Debug(vEntry.Substring(1));
#endif
                                }

                                _commandStack.Push(entry[0].ToInvariantString());
#if DEBUG_PATH
                                 Logger.Debug(vEntry[0].ToString());
#endif
                            }
                            else
                            {
                                _commandStack.Push(entry);
#if DEBUG_PATH
                                Logger.Debug(vEntry);
#endif
                            }
                        }

                        _commandStack.Push(c.ToInvariantString());
#if DEBUG_PATH
                        Logger.Debug(vChar.ToString());
#endif
                    }
                    else
                    {
                        _commandStack.Push(entry);
#if DEBUG_PATH
                        Logger.Debug(vEntry);
#endif
                    }
                }

                while (_commandStack.Count > 0)
                {
                    if (_path == null)
                    {
                        _path = new PathF();
                    }

                    string topCommand = _commandStack.Pop();
                    var firstLetter = topCommand[0];
                    
                    if (IsCommand(firstLetter))
                        HandleCommand(topCommand);
                    else
                    {
                        _commandStack.Push(topCommand);
                        HandleCommand(_lastCommand.ToString());
                    }
                }

                if (_path != null && !_path.Closed)
                {
                    if (_closeWhenDone)
                    {
                        _path.Close();
                    }
                }
            }
            catch (Exception exc)
            {
#if DEBUG
                Logger.Debug("=== An error occured parsing the path. ===", exc);
                Logger.Debug(pathAsString);
                throw;
#endif
            }

            return _path;
        }

        private bool IsCommand(char firstLetter)
        {
            if (char.IsDigit(firstLetter))
                return false;

            if (firstLetter == '.')
                return false;
            
            if (firstLetter == '-')
                return false;
            
            if (firstLetter == 'e' || firstLetter == 'E')
                return false;

            return true;
        }

        private void HandleCommand(string command)
        {
            char c = command[0];

            if (_lastCommand != '~' && (char.IsDigit(c) || c == '-'))
            {
                var previousCommand = _commandStack.Peek()?[0];

                if (_lastCommand == 'M')
                {
                    _commandStack.Push(command);
                    HandleCommand('L');
                }
                else if (_lastCommand == 'm')
                {
                    _commandStack.Push(command);
                    HandleCommand('l');
                }
                else if (_lastCommand == 'L')
                {
                    _commandStack.Push(command);
                    HandleCommand('L');
                }
                else if (_lastCommand == 'l')
                {
                    _commandStack.Push(command);
                    HandleCommand('l');
                }
                else if (_lastCommand == 'H')
                {
                    _commandStack.Push(command);
                    HandleCommand('H');
                }
                else if (_lastCommand == 'h')
                {
                    _commandStack.Push(command);
                    HandleCommand('h');
                }
                else if (_lastCommand == 'V')
                {
                    _commandStack.Push(command);
                    HandleCommand('V');
                }
                else if (_lastCommand == 'v')
                {
                    _commandStack.Push(command);
                    HandleCommand('v');
                }
                else if (_lastCommand == 'C')
                {
                    _commandStack.Push(command);
                    HandleCommand('C');
                }
                else if (_lastCommand == 'c')
                {
                    _commandStack.Push(command);
                    HandleCommand('c');
                }
                else if (_lastCommand == 'S')
                {
                    _commandStack.Push(command);
                    HandleCommand('S');
                }
                else if (_lastCommand == 's')
                {
                    _commandStack.Push(command);
                    HandleCommand('s');
                }
                else if (_lastCommand == 'Q')
                {
                    _commandStack.Push(command);
                    HandleCommand('Q');
                }
                else if (_lastCommand == 'q')
                {
                    _commandStack.Push(command);
                    HandleCommand('q');
                }
                else if (_lastCommand == 'T')
                {
                    _commandStack.Push(command);
                    HandleCommand('T', previousCommand);
                }
                else if (_lastCommand == 't')
                {
                    _commandStack.Push(command);
                    HandleCommand('t', previousCommand);
                }
                else if (_lastCommand == 'A')
                {
                    _commandStack.Push(command);
                    HandleCommand('A');
                }
                else if (_lastCommand == 'a')
                {
                    _commandStack.Push(command);
                    HandleCommand('a');
                }
                else
                {
                    Logger.Debug("Don't know how to handle the path command: " + command);
                }
            }
            else
            {
                HandleCommand(c);
            }
        }

        private void HandleCommand(char command, char? previousCommand = null)
        {
            if (command == 'M')
            {
                MoveTo(false);
            }
            else if (command == 'm')
            {
                MoveTo(true);
                if (_lastCommand == '~')
                {
                    command = 'm';
                }
            }
            else if (command == 'z' || command == 'Z')
            {
                ClosePath();
            }
            else if (command == 'L')
            {
                LineTo(false);
            }
            else if (command == 'l')
            {
                LineTo(true);
            }
            else if (command == 'Q')
            {
                QuadTo(false);
            }
            else if (command == 'q')
            {
                QuadTo(true);
            }
            else if (command == 'T')
            {
                ReflectiveQuadTo(false, previousCommand);
            }
            else if (command == 't')
            {
                ReflectiveQuadTo(true, previousCommand);
            }
            else if (command == 'C')
            {
                CurveTo(false);
            }
            else if (command == 'c')
            {
                CurveTo(true);
            }
            else if (command == 'S')
            {
                SmoothCurveTo(false);
            }
            else if (command == 's')
            {
                SmoothCurveTo(true);
            }
            else if (command == 'A')
            {
                ArcTo(false);
            }
            else if (command == 'a')
            {
                ArcTo(true);
            }
            else if (command == 'H')
            {
                HorizontalLineTo(false);
            }
            else if (command == 'h')
            {
                HorizontalLineTo(true);
            }
            else if (command == 'V')
            {
                VerticalLineTo(false);
            }
            else if (command == 'v')
            {
                VerticalLineTo(true);
            }
            else
            {
                Logger.Debug("Don't know how to handle the path command: " + command);
            }

            if (!(command == 'C' || command == 'c' || command == 's' || command == 'S'))
            {
                _lastCurveControlPoint = null;
            }

            _lastCommand = command;
        }

        private void ClosePath()
        {
            _path.Close();
            _relativePoint = _lastMoveTo;

            /*int vSegments = path.SegmentCount;
         if (vSegments >= 2)
         {
            if (path.GetSegmentType(vSegments-2) == PathOperation.MOVE_TO && path.GetSegmentType(vSegments-1) == PathOperation.CLOSE)
            {
               path.RemoveAllSegmentsAfter(vSegments-2);
            }
         }*/
        }

        private void MoveTo(bool isRelative)
        {
            if (_path.SubPathCount == 1)
            {
                if (_path.FirstPoint.Equals(_path.LastPoint))
                {
                    _closeWhenDone = true;
                }
            }

            var xOffset = NextValue;
            var yOffset = NextValue;
            var point = NewPoint(xOffset, yOffset, isRelative, true);
            _path.MoveTo(point);
            _lastMoveTo = point;
        }

        private void LineTo(bool isRelative)
        {
            var point = NewPoint(NextValue, NextValue, isRelative, true);
            _path.LineTo(point);
        }

        private void HorizontalLineTo(bool isRelative)
        {
            var point = NewHorizontalPoint(NextValue, isRelative, true);
            _path.LineTo(point);
        }

        private void VerticalLineTo(bool isRelative)
        {
            var point = NewVerticalPoint(NextValue, isRelative, true);
            _path.LineTo(point);
        }

        private void CurveTo(bool isRelative)
        {
            var point1 = NewPoint(NextValue, NextValue, isRelative, false);
            float x = NextValue;
            float y = NextValue;

            bool isQuad = char.IsLetter(_commandStack.Peek()[0]);
            var point2 = NewPoint(x, y, isRelative, isQuad);

            if (isQuad)
            {
                _path.QuadTo(point1, point2);
                _lastCurveControlPoint = point1;
            }
            else
            {
                var point3 = NewPoint(NextValue, NextValue, isRelative, true);
                _path.CurveTo(point1, point2, point3);
                _lastCurveControlPoint = point2;
                //Logger.Debug($"CurveTo({point1.X},{point1.Y},{point2.X},{point2.Y},{point3.X},{point3.Y})");
            }
        }

        private void QuadTo(bool isRelative)
        {
            var point1 = NewPoint(NextValue, NextValue, isRelative, false);
            var point2 = NewPoint(NextValue, NextValue, isRelative, true);
            _lastCurveControlPoint = point1;
            _path.QuadTo(point1, point2);
        }
        
        private void ReflectiveQuadTo(bool isRelative, char? previousCommand)
        {
            var lastPoint = _path.LastPoint;
            var point1 = lastPoint;
            var lastCurveControlPoint = _lastCurveControlPoint ?? default;
            switch (previousCommand)
            {
                case 'Q':
                case 'q':
                case 'T':
                case 't':
                    var dx = lastPoint.X - lastCurveControlPoint.X;
                    var dy = lastPoint.Y - lastCurveControlPoint.Y;
                    point1 = point1.Offset(dx, dy);
                    break;
            }
            var point2 = NewPoint(NextValue, NextValue, isRelative, true);
            _lastCurveControlPoint = point1;
            _path.QuadTo(point1, point2);
        }

        private void SmoothCurveTo(bool isRelative)
        {
            PointF? point1 = null;
            var point2 = NewPoint(NextValue, NextValue, isRelative, false);

            // ReSharper disable ConvertIfStatementToNullCoalescingExpression
            if (_lastCurveControlPoint == null && _relativePoint != null)
            {
                // ReSharper restore ConvertIfStatementToNullCoalescingExpression
                point1 = Geometry.GetOppositePoint((PointF)_relativePoint, point2);
            }
            else if (_relativePoint != null && _lastCurveControlPoint != null)
            {
                point1 = Geometry.GetOppositePoint((PointF)_relativePoint, (PointF)_lastCurveControlPoint);
            }

            var point3 = NewPoint(NextValue, NextValue, isRelative, true);
            if (point1 != null)
                _path.CurveTo((PointF)point1, point2, point3);
            _lastCurveControlPoint = point2;
        }
        
        private void ArcTo(bool isRelative)
        {
            var startPoint = _relativePoint ?? default;

            var rx = NextValue;
            var ry = NextValue;

            var r = NextValue;
            var largeArcFlag = NextBoolValue;
            var sweepFlag = NextBoolValue;
            var endPoint = NewPoint(NextValue, NextValue, isRelative, false);

            var arcPath = new PathF(startPoint);
            arcPath.SVGArcTo(rx, ry, r, largeArcFlag, sweepFlag, endPoint.X, endPoint.Y, startPoint.X, startPoint.Y);

            for (int s = 0; s < arcPath.OperationCount; s++)
            {
                var segmentType = arcPath.GetSegmentType(s);
                var pointsInSegment = arcPath.GetPointsForSegment(s);

                if (segmentType == PathOperation.Move)
                {
                    // do nothing   
                }
                else if (segmentType == PathOperation.Line)
                {
                    _path.LineTo(pointsInSegment[0]);
                }
                else if (segmentType == PathOperation.Cubic)
                {
                    _path.CurveTo(pointsInSegment[0], pointsInSegment[1], pointsInSegment[2]);
                }
                else if (segmentType == PathOperation.Quad)
                {
                    _path.QuadTo(pointsInSegment[0], pointsInSegment[1]);
                }
            }

            _relativePoint = _path.LastPoint;
        }

        private PointF NewPoint(float x, float y, bool isRelative, bool isReference)
        {
            PointF point = default;

            if (isRelative && _relativePoint != null)
            {
                point = new PointF(((PointF)_relativePoint).X + x, ((PointF)_relativePoint).Y + y);
            }
            else
            {
                point = new PointF(x, y);
            }

            // If this is the reference point, we want to store the location before
            // we translate it into the final coordinates.  This way, future relative
            // points will start from an un-translated position.
            if (isReference)
            {
                _relativePoint = point;
            }

            return point;
        }

        private PointF NewVerticalPoint(float y, bool isRelative, bool isReference)
        {
            PointF point = default;

            if (isRelative && _relativePoint != null)
            {
                point = new PointF(((PointF)_relativePoint).X, ((PointF)_relativePoint).Y + y);
            }
            else if (_relativePoint != null)
            {
                point = new PointF(((PointF)_relativePoint).X, y);
            }

            // If this is the reference point, we want to store the location before
            // we translate it into the final coordinates.  This way, future relative
            // points will start from an un-translated position.
            if (isReference)
            {
                _relativePoint = point;
            }

            return point;
        }

        private PointF NewHorizontalPoint(float x, bool isRelative, bool isReference)
        {
            PointF point = default;

            if (isRelative && _relativePoint != null)
            {
                point = new PointF(((PointF)_relativePoint).X + x, ((PointF)_relativePoint).Y);
            }
            else if (_relativePoint != null)
            {
                point = new PointF(x, ((PointF)_relativePoint).Y);
            }

            // If this is the reference point, we want to store the location before
            // we translate it into the final coordinates.  This way, future relative
            // points will start from an un-translated position.
            if (isReference)
            {
                _relativePoint = point;
            }

            return point;
        }
    }
}